using MemoryPack;
using RoslynCSharp;
using RoslynCSharp.Compiler;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Utils;
using UnityEngine;
using System.Collections;

namespace Game {
	public sealed class ComputingUnit {
		public const string DefaultProgramFileName = "Program";

		ComputingUnitState _state;

		public ComputingUnitState State {
			get {
				return _state;
			}
			set {
				if ( _state != value) {
					_state = value;
					OnStateChanged.Fire();
				}				
			}
		}
		public string               OwnerUser    => Data.OwnerUser;
		public uint                 ID           => Data.InstanceId;
		public ComputingUnitData    Data         = new ComputingUnitData();
		public ComputerTextTerminal TextTerminal = new ComputerTextTerminal();
		public string               ErrorOutput;

		double         _lastRunTime;
		IEnumerator    _iterator;
		ScriptDomain   _domain = null;
		ScriptProxy    _mainObjProxy;
		ScriptAssembly _scriptAssembly;
		ScriptType     _mainProgramType;

		ShipAPI    _shipApi;
		GlobalData _global;
		UnitMemory _memory;
		UnitStats  _stats;

		AsyncCompileOperation _compileOperation;

		public EventBus         OnStateChanged            = new EventBus();
		public EventBus         OnFileListChanged         = new EventBus();
		public EventBus<uint>   OnAttachedEntityIdChanged = new EventBus<uint>();
		public EventBus<string> OnCodeFileUpdated         = new EventBus<string>();
		
		public void Create(uint instanceId, string ownerUser) {
			Data.OwnerUser  = ownerUser;
			Data.InstanceId = instanceId;
		}

		public void Init() {
			_domain = ScriptDomain.CreateDomain($"ScriptingDomain_{Data.InstanceId}", true);
			
			foreach ( AssemblyReferenceAsset reference in CommonStaticData.Instance.DefaultScriptAssemlies ) {
				_domain.RoslynCompilerService.ReferenceAssemblies.Add(reference);
			}

			if ( Data.SourceCode.Count == 0 ) {
				Data.SourceCode.Add(DefaultProgramFileName, CommonStaticData.Instance.DefaultProgramText.text);
				Data.DefaultProgramFile = DefaultProgramFileName;
			}

			State = ComputingUnitState.DomainCreated;
		}

		public void Load(ComputingUnitData data) {
			if ( data == null) {
				return;
			}
			Data = data;
		}

		public ComputingUnitData Save() {
			Data.LastTerminalMessages = TextTerminal.Messages;
			if ( State == ComputingUnitState.Active ) {
				ProcessProgramShutdown();
			}
			return Data;
		}

		public void UpdateAttachedEntityId(uint id) {
			if ( Data.AttachedEntity == id ) {
				return;
			}
			Data.AttachedEntity = id;
			OnAttachedEntityIdChanged.Fire(id);
		}

		public void AddOrReplaceSourceCode(string fileName, string source) {
			if ( string.IsNullOrEmpty(fileName) ) {
				return;
			}
			if ( Data.SourceCode.ContainsKey(fileName) ) {
				Data.SourceCode[fileName] = source;
			} else {
				Data.SourceCode.Add(fileName, source);
				OnFileListChanged.Fire();
			}
			OnCodeFileUpdated.Fire(fileName);
		}

		public void RemoveSourceCode(string fileName) {
			if ( string.IsNullOrEmpty(fileName) ) {
				return;
			}
			if ( Data.SourceCode.ContainsKey(fileName) ) {
				Data.SourceCode.Remove(fileName);
			}
			OnFileListChanged.Fire();
		}

		public string GetSourceCodeFile(string fileName) {
			if ( string.IsNullOrEmpty(fileName) ) {
				return null;
			}
			if ( Data.SourceCode.TryGetValue(fileName, out var file) ) {
				return file;
			}
			return null;
		}

		public bool HasSourceCodeFile(string fileName) {
			if ( string.IsNullOrEmpty(fileName) ) {
				return false;
			}
			return Data.SourceCode.ContainsKey(fileName);
		}

		public List<string> GetSourceCodeFileNames() {
			var result = new List<string>();
			foreach ( var script in Data.SourceCode ) {
				result.Add(script.Key);
			}
			return result;
		}

		public void SetupProgram(GlobalData globalsObject, ShipAPI shipObject, uint attachedEntityId) {
			ClearRuntimeData();
			try {
				_global      = globalsObject;
				_lastRunTime = _global.Time;
				_shipApi     = shipObject;
				_memory      = new UnitMemory(this);
				_stats       = new UnitStats(this);
				_mainObjProxy = _mainProgramType.CreateInstance();
				_mainObjProxy.Fields.SetValue("Ship",      _shipApi);
				_mainObjProxy.Fields.SetValue("Global",       _global);
				_mainObjProxy.Fields.SetValue("Memory",       _memory);
				_mainObjProxy.Fields.SetValue("TextTerminal", TextTerminal);
				_mainObjProxy.Fields.SetValue("Stats", _stats);
				Data.AttachedEntity = attachedEntityId;
				_mainObjProxy.Call("Start");
				State = ComputingUnitState.Active;
			} catch (Exception e) {
				ErrorOutput = e.ToString();
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.RuntimeError;
			}			
		}

		public void RunUpdate() {
			if ( State != ComputingUnitState.Active ) {
				return;
			}
			try {
				if ( _iterator != null ) {
					CheckIterator();
				} else {
					var result = _mainObjProxy.Call("Update", ProxyCallConvention.StandardMethod, (float)(_global.Time - _lastRunTime));
					if ( result is IEnumerator ) {
						_iterator = (IEnumerator)result;
					}
				}

			} catch (Exception e) {
				ErrorOutput = e.ToString();
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.RuntimeError;
				ClearRuntimeData();
			}
		}

		void CheckIterator() {
			if ( !(_iterator.Current is WaitInstruction) ) {
				var canContinue = _iterator.MoveNext();
				if ( !canContinue ) {
					_iterator = null;
					return;
				}
				return;
			}
			var instruction = (WaitInstruction)_iterator.Current;
			if ( instruction == null ) {
				return;
			}
			var shouldContinue = instruction.Check();
			if ( shouldContinue ) {
				if ( !_iterator.MoveNext() ) {
					_iterator = null;
					return;
				}
			}
		}

		public void ProcessConsoleMessage(string message) {
			if ( State != ComputingUnitState.Active ) {
				return;
			}
			try {
				TextTerminal.AddMessage(message);
				_mainObjProxy.Call("ConsoleInput", message);
			} catch (Exception e) {
				ErrorOutput = e.ToString();
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.RuntimeError;
				ClearRuntimeData();
			}
		}

		public void RunMethod(string methodName, string args) {
			if ( State != ComputingUnitState.Active || string.IsNullOrEmpty(methodName) ) {
				return;
			}
			try {
				if ( args ==null ) {
					_mainObjProxy.Call(methodName);
				} else {
					_mainObjProxy.Call(methodName, args);
				}				
			}	catch ( Exception e ) {
				if ( e is TargetException ) {
					TextTerminal.AddErrorMessage(e.ToString());
					return;
				}
				ErrorOutput = e.ToString();
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.RuntimeError;
				ClearRuntimeData();
			}
		}

		public void ProcessProgramShutdown() {
			if ( State != ComputingUnitState.Active ) {
				return;
			}
			try {
				_mainObjProxy.Call("OnProgramShutdown");
			} catch ( Exception e ) {
				ErrorOutput = e.ToString();
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.RuntimeError;
				ClearRuntimeData();
			}
			State = ComputingUnitState.Paused;
		}

		public void Compile() {
			ClearRuntimeData();
			if ( _domain != null && State != ComputingUnitState.Uninitialized ) {
				_compileOperation = _domain.CompileAndLoadSourcesAsync(Data.SourceCode.Values.ToArray(), ScriptSecurityMode.UseSettings);
				State = ComputingUnitState.Compilation;
			}
		}

		public bool CheckCompileResult() {
			if ( _compileOperation == null ) {
				ErrorOutput = $"Compile operation was started, but there's no compile operation obect. Unit instance id: {Data.InstanceId}";
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.CompileError;
				return true;
			}
			if ( _compileOperation.MoveNext() ) {
				return false;
			}
			return true;
		}

		public void FinalizeCompilation() {
			if ( _compileOperation == null ) {
				ErrorOutput = $"Compile operation was started, but there's no compile operation obect. Unit instance id: {Data.InstanceId}";
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.CompileError;
			}
			if ( !_compileOperation.IsDone) {
				return;
			}
			if ( !_compileOperation.IsSuccessful ) {
				// Get all errors
				var sb = new StringBuilder();
				foreach ( CompilationError error in _compileOperation.CompileDomain.CompileResult.Errors ) {
					if ( error.IsError == true ) {
						sb.Append("Err:");
						sb.AppendLine( error.ToString() );
						TextTerminal.AddErrorMessage(error.ToString());
					} else if ( error.IsWarning == true ) {
						Debug.LogWarning(error.ToString()); //TODO: Disable
					}
				}
				ErrorOutput = sb.ToString();
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.CompileError;
				return;
			}
			if ( !_compileOperation.IsSecurityVerified && RoslynCSharp.RoslynCSharp.Settings.SecurityCheckCode ) {
				TextTerminal.AddErrorMessage("Security check error");
				State = ComputingUnitState.CompileError;
				return;
			}
			_scriptAssembly = _compileOperation.CompiledAssembly;
			if ( _scriptAssembly == null || !_scriptAssembly.CompileResult.Success ) {
				TextTerminal.AddErrorMessage("Compilation Error");
				State = ComputingUnitState.CompileError;
				return;
			}
			//var mainProgramTypes = _scriptAssembly.FindAllSubTypesOf<ShipProgram>();
			_mainProgramType = _scriptAssembly.FindSubTypeOf<ShipProgram>();
			if ( _mainProgramType == null ) {
				ErrorOutput = "Compilation Error: No ShipProgram-derived type found in assembly";
				TextTerminal.AddErrorMessage(ErrorOutput);
				State = ComputingUnitState.CompileError;
				return;
			}
			State = ComputingUnitState.Ready;
		}

		public void BreakCoroutine() {
			_iterator = null;
		}

		public void ClearRuntimeData() {
			if ( _mainObjProxy != null ) {
				_mainObjProxy.Dispose();
			}
			_mainObjProxy = null;
			_shipApi = null;
			_global = null;
			_memory = null;
			_iterator = null;
		}
	}

	[MemoryPackable]
	public sealed partial class ComputingUnitData {
		public uint                       InstanceId;
		public string                     OwnerUser;
		public uint                       AttachedEntity;
		public Dictionary<string, string> SourceCode           = new Dictionary<string, string>();
		public Dictionary<string, string> UserParams           = new Dictionary<string, string>();
		public List<string>               LastTerminalMessages = new List<string>();
		public string                     DefaultProgramFile;		
	}
}
